//-----------------------------------------------------------------------
// <copyright file="Subscription.cs" company="iron9light">
//      Copyright (c) iron9light. All rights reserved.
// </copyright>
// <author>iron9light</author>
//-----------------------------------------------------------------------

namespace EventBroker
{
    using System;
    using System.Collections.Generic;
    using System.Globalization;
    using System.Reflection;

    /// <summary>
    /// Represents a topic subscription.
    /// </summary>
    public class Subscription : ISubscription
    {
        private readonly WeakReference weakRefSubscriber;
        private readonly string handlerMethodName;
        private readonly RuntimeMethodHandle methodHandle;
        private readonly Type handlerEventArgsType;
        private readonly RuntimeTypeHandle typeHandle;

        /// <summary>
        /// Initializes a new instance of the <see cref="Subscription"/> class.
        /// </summary>
        /// <param name="subscriber">The subscriber.</param>
        /// <param name="handlerMethodName">Name of the handler method.</param>
        internal Subscription(object subscriber, string handlerMethodName)
            : this(subscriber, handlerMethodName, null)
        {
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="Subscription"/> class
        /// </summary>
        /// <param name="subscriber">The subscriber.</param>
        /// <param name="handlerMethodName">Name of the handler method.</param>
        /// <param name="parameterTypes">The parameter types.</param>
        internal Subscription(
                object subscriber,
                string handlerMethodName,
                Type[] parameterTypes)
        {
            this.weakRefSubscriber = new WeakReference(subscriber);
            this.handlerMethodName = handlerMethodName;

            MethodInfo methodInfo = GetMethodInfo(subscriber, handlerMethodName, parameterTypes);

            GuardMethodExist(methodInfo, subscriber, handlerMethodName);
            GuardMethodNotStatic(methodInfo);
            GuardMethodNotGeneric(methodInfo);
            GuardMethodHasNoReturn(methodInfo);
            GuardMethodHasValidParams(methodInfo);

            this.typeHandle = subscriber.GetType().TypeHandle;
            this.methodHandle = methodInfo.MethodHandle;

            Type eventArgsType = methodInfo.GetParameters()[1].ParameterType;
            this.handlerEventArgsType = typeof(EventHandler<>).MakeGenericType(eventArgsType);
        }

        /// <summary>
        /// Gets the subscriber of the event.
        /// </summary>
        public object Subscriber
        {
            get { return this.weakRefSubscriber.Target; }
        }

        /// <summary>
        /// Gets the handler method name that's subscribed to the event.
        /// </summary>
        public string HandlerMethodName
        {
            get { return this.handlerMethodName; }
        }

        /// <summary>
        /// Gets the handler.
        /// </summary>
        /// <returns>The callback.</returns>
        public EventTopicFireHandler Handler
        {
            get { return this.OnEventTopicFire; }
        }

        /// <summary>
        /// Called when event topic fire.
        /// </summary>
        /// <param name="sender">The sender.</param>
        /// <param name="e">The <see cref="System.EventArgs"/> instance containing the event data.</param>
        /// <param name="exceptions">The exceptions.</param>
        protected virtual void OnEventTopicFire(object sender, EventArgs e, ICollection<Exception> exceptions)
        {
            object subscriber = this.weakRefSubscriber.Target;
            if (subscriber == null)
            {
                return;
            }

            try
            {
                Delegate handler = this.CreateSubscriptionDelegate();
                if (handler != null)
                {
                    handler.DynamicInvoke(
                            sender,
                            e);
                }
            }
            catch (TargetInvocationException ex)
            {
                exceptions.Add(ex.InnerException);
            }
        }

        /// <summary>
        /// Creates the subscription delegate.
        /// </summary>
        /// <returns>The delegate.</returns>
        protected Delegate CreateSubscriptionDelegate()
        {
            object subscriber = this.weakRefSubscriber.Target;
            if (subscriber != null)
            {
                MethodInfo methodInfo = (MethodInfo)MethodBase.GetMethodFromHandle(
                                                             this.methodHandle,
                                                             this.typeHandle);
                return Delegate.CreateDelegate(
                        this.handlerEventArgsType,
                        subscriber,
                        methodInfo);
            }

            return null;
        }

        /// <summary>
        /// Gets the method info.
        /// </summary>
        /// <param name="subscriber">The subscriber.</param>
        /// <param name="handlerMethodName">Name of the handler method.</param>
        /// <param name="parameterTypes">The parameter types.</param>
        /// <returns>The method info.</returns>
        private static MethodInfo GetMethodInfo(object subscriber, string handlerMethodName, Type[] parameterTypes)
        {
            if (parameterTypes != null)
            {
                return subscriber.GetType().GetMethod(
                        handlerMethodName,
                        parameterTypes);
            }

            return subscriber.GetType().GetMethod(handlerMethodName);
        }

        /// <summary>
        /// Guards the method is not static.
        /// </summary>
        /// <param name="methodInfo">The method info.</param>
        private static void GuardMethodNotStatic(MethodInfo methodInfo)
        {
            if (methodInfo.IsStatic)
            {
                throw new EventBrokerException(
                        String.Format(
                                CultureInfo.CurrentCulture,
                                Properties.Resources.CannotRegisterStaticSubscriptionMethods,
                                methodInfo.DeclaringType.FullName,
                                methodInfo.Name));
            }
        }

        /// <summary>
        /// Guards the method exists.
        /// </summary>
        /// <param name="methodInfo">The method info.</param>
        /// <param name="subscriber">The subscriber.</param>
        /// <param name="handlerMethodName">Name of the handler method.</param>
        private static void GuardMethodExist(MethodInfo methodInfo, object subscriber, string handlerMethodName)
        {
            if (methodInfo == null)
            {
                throw new EventBrokerException(
                        String.Format(
                                CultureInfo.CurrentCulture,
                                Properties.Resources.SubscriberHandlerNotFound,
                                handlerMethodName,
                                subscriber.GetType()));
            }
        }

        /// <summary>
        /// Guards the method is not open generic.
        /// </summary>
        /// <param name="method">The method.</param>
        private static void GuardMethodNotGeneric(MethodInfo method)
        {
            if (method.IsGenericMethodDefinition)
            {
                throw new EventBrokerException(
                        String.Format(
                                CultureInfo.CurrentCulture,
                                Properties.Resources.CannotRegisterOpenGenericSubscriptionMethods,
                                method.DeclaringType.FullName,
                                method.Name));
            }
        }

        /// <summary>
        /// Guards the method has no non-void return value.
        /// </summary>
        /// <param name="method">The method.</param>
        private static void GuardMethodHasNoReturn(MethodInfo method)
        {
            if (method.ReturnType != typeof(void))
            {
                throw new EventBrokerException(
                        String.Format(
                                CultureInfo.CurrentCulture,
                                Properties.Resources.CannotRegisterSubscriptionMethodsReturnNonVoidValue,
                                method.DeclaringType.FullName,
                                method.Name));
            }
        }

        /// <summary>
        /// Guards the method has valid parameters.
        /// </summary>
        /// <param name="method">The method.</param>
        private static void GuardMethodHasValidParams(MethodInfo method)
        {
            if (!IsValidMethodParameters(method))
            {
                throw new EventBrokerException(
                        String.Format(
                                CultureInfo.CurrentCulture,
                                Properties.Resources.InvalidSubscriptionSignature,
                                method.DeclaringType.FullName,
                                method.Name));
            }
        }

        /// <summary>
        /// Determines whether the specified parameters of the method is valid event handler parameters.
        /// </summary>
        /// <param name="method">The method.</param>
        /// <returns>
        ///     <c>true</c> if the specified parameters is valid event handler parameters; otherwise, <c>false</c>.
        /// </returns>
        private static bool IsValidMethodParameters(MethodInfo method)
        {
            ParameterInfo[] parameters = method.GetParameters();

            if (parameters.Length != 2)
            {
                return false;
            }

            if (!typeof(object).IsAssignableFrom(parameters[0].ParameterType))
            {
                return false;
            }

            if (!typeof(EventArgs).IsAssignableFrom(parameters[1].ParameterType))
            {
                return false;
            }

            return true;
        }
    }
}